feat: add support for recurring tasks - #1611
Conversation
- Introduced new fields for recurrence interval unit and count in task model. - Updated task creation and update requests to include recurrence parameters. - Implemented logic to handle overdue recurring tasks in the task worker. - Added validation to ensure both recurrence unit and count are set together. - Created new SQL migration for the recurrence interval unit type and count columns. - Enhanced GraphQL schema to support recurrence fields in task queries and mutations. - Added tests for next recurrence deadline calculation and task worker functionality. Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
There was a problem hiding this comment.
10 issues found across 23 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="pkg/probo/recurring_task_worker.go">
<violation number="1" location="pkg/probo/recurring_task_worker.go:80">
P1: A malformed recurrence row can crash or wedge the recurring-task worker: `recurrence_interval_count` is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make `nextRecurrenceDeadline` loop forever while its transaction remains open.</violation>
</file>
<file name="pkg/cmd/task/update/update.go">
<violation number="1" location="pkg/cmd/task/update/update.go:139">
P2: Clearing either recurrence flag by itself fails for existing recurring tasks instead of clearing recurrence. Clear both interval fields when either flag requests a clear, or reject incomplete clear requests locally.</violation>
</file>
<file name="pkg/coredata/task.go">
<violation number="1" location="pkg/coredata/task.go:51">
P1: Measure imports now fail at `Task.Upsert`: strict `RowToStructByName[Task]` cannot find either new recurrence field in its `RETURNING` row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in `RETURNING`.</violation>
</file>
<file name="apps/console/src/components/tasks/TasksCard.tsx">
<violation number="1" location="apps/console/src/components/tasks/TasksCard.tsx:627">
P2: The recurring badge icon uses a `<span title="...">` to convey recurrence information, but screen readers won't reliably announce this. The embedded `<IconRotateCw />` SVG likely has no accessible name (no `aria-label`, `role="img"`), and the `<span>` isn't keyboard-focusable. Consider adding `aria-label` directly on the icon or using a visually-hidden text element for screen reader users, matching the pattern used by `<Button>` with `title` in this same component.</violation>
</file>
<file name="apps/console/src/_locales/fr-FR.json">
<violation number="1" location="apps/console/src/_locales/fr-FR.json:1429">
P2: The recurringBadge tooltip renders improperly in French for singular counts. `tooltip_one` should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").</violation>
<violation number="2" location="apps/console/src/_locales/fr-FR.json:1430">
P3: The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (`2 Semaine`, `2 Année`). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.</violation>
</file>
<file name="apps/console/src/components/tasks/TaskFormDialog.tsx">
<violation number="1" location="apps/console/src/components/tasks/TaskFormDialog.tsx:211">
P1: Non-recurring tasks cannot be submitted with the new defaults: the form starts with count `1` and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores `1`; initializing the count as `null` when no unit is present (or clearing it when the unit becomes `none`) would preserve the intended optional recurrence behavior.</violation>
<violation number="2" location="apps/console/src/components/tasks/TaskFormDialog.tsx:464">
P1: Opening/rendering this recurrence selector throws because Radix `Select.Item` disallows `value=""`. Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.</violation>
</file>
<file name="packages/n8n-node/nodes/Probo/actions/task/update.operation.ts">
<violation number="1" location="packages/n8n-node/nodes/Probo/actions/task/update.operation.ts:234">
P1: Updating recurrence always sends the count as a GraphQL string, so values such as `3` fail `Int` input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.</violation>
</file>
<file name="e2e/console/task_test.go">
<violation number="1" location="e2e/console/task_test.go:983">
P2: Add a subtest that creates a task with a deadline and then updates it with recurrence fields, asserting that recurrenceIntervalUnit and recurrenceIntervalCount are set correctly in the response. This would catch bugs in the update resolver's handling of recurrence fields, which is currently only exercised through the error path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| { | ||
| displayName: 'Recurrence Interval Count', | ||
| name: 'recurrenceIntervalCount', | ||
| type: 'string', |
There was a problem hiding this comment.
P1: Updating recurrence always sends the count as a GraphQL string, so values such as 3 fail Int input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/n8n-node/nodes/Probo/actions/task/update.operation.ts, line 234:
<comment>Updating recurrence always sends the count as a GraphQL string, so values such as `3` fail `Int` input coercion before the mutation runs. Use a numeric n8n parameter (and preserve the empty default for unchanged) to send an integer.</comment>
<file context>
@@ -193,6 +193,54 @@ export const description: INodeProperties[] = [
+ {
+ displayName: 'Recurrence Interval Count',
+ name: 'recurrenceIntervalCount',
+ type: 'string',
+ displayOptions: {
+ show: {
</file context>
| } | ||
|
|
||
| unit := *task.RecurrenceIntervalUnit | ||
| count := *task.RecurrenceIntervalCount |
There was a problem hiding this comment.
P1: A malformed recurrence row can crash or wedge the recurring-task worker: recurrence_interval_count is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make nextRecurrenceDeadline loop forever while its transaction remains open.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/probo/recurring_task_worker.go, line 80:
<comment>A malformed recurrence row can crash or wedge the recurring-task worker: `recurrence_interval_count` is nullable and unconstrained in the migration, while selection only requires a unit. Validate this invariant at the database boundary (and/or skip invalid rows) before dereferencing, since zero or negative counts also make `nextRecurrenceDeadline` loop forever while its transaction remains open.</comment>
<file context>
@@ -0,0 +1,166 @@
+ }
+
+ unit := *task.RecurrenceIntervalUnit
+ count := *task.RecurrenceIntervalCount
+ deadline := nextRecurrenceDeadline(*task.Deadline, unit, count, now)
+
</file context>
| value={field.value ?? ""} | ||
| onValueChange={field.onChange} | ||
| > | ||
| <Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option> |
There was a problem hiding this comment.
P1: Opening/rendering this recurrence selector throws because Radix Select.Item disallows value="". Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 464:
<comment>Opening/rendering this recurrence selector throws because Radix `Select.Item` disallows `value=""`. Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.</comment>
<file context>
@@ -393,6 +438,39 @@ export default function TaskFormDialog(props: Props) {
+ value={field.value ?? ""}
+ onValueChange={field.onChange}
+ >
+ <Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option>
+ <Option value="DAY">{t("taskFormDialog.recurrenceIntervalUnits.day")}</Option>
+ <Option value="WEEK">{t("taskFormDialog.recurrenceIntervalUnits.week")}</Option>
</file context>
| TimeEstimate *time.Duration `db:"time_estimate"` | ||
| AssignedToID *gid.GID `db:"assigned_to_profile_id"` | ||
| Deadline *time.Time `db:"deadline"` | ||
| RecurrenceIntervalUnit *TaskRecurrenceIntervalUnit `db:"recurrence_interval_unit"` |
There was a problem hiding this comment.
P1: Measure imports now fail at Task.Upsert: strict RowToStructByName[Task] cannot find either new recurrence field in its RETURNING row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in RETURNING.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/coredata/task.go, line 51:
<comment>Measure imports now fail at `Task.Upsert`: strict `RowToStructByName[Task]` cannot find either new recurrence field in its `RETURNING` row. Include recurrence columns and args in the upsert insert/conflict clauses and both columns in `RETURNING`.</comment>
<file context>
@@ -37,20 +37,22 @@ import (
+ TimeEstimate *time.Duration `db:"time_estimate"`
+ AssignedToID *gid.GID `db:"assigned_to_profile_id"`
+ Deadline *time.Time `db:"deadline"`
+ RecurrenceIntervalUnit *TaskRecurrenceIntervalUnit `db:"recurrence_interval_unit"`
+ RecurrenceIntervalCount *int `db:"recurrence_interval_count"`
+ Rank int `db:"rank"`
</file context>
| measureId: task?.measure?.id ?? measureId ?? "", | ||
| deadline: task?.deadline?.split("T")[0] ?? "", | ||
| recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "", | ||
| recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1, |
There was a problem hiding this comment.
P1: Non-recurring tasks cannot be submitted with the new defaults: the form starts with count 1 and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores 1; initializing the count as null when no unit is present (or clearing it when the unit becomes none) would preserve the intended optional recurrence behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 211:
<comment>Non-recurring tasks cannot be submitted with the new defaults: the form starts with count `1` and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores `1`; initializing the count as `null` when no unit is present (or clearing it when the unit becomes `none`) would preserve the intended optional recurrence behavior.</comment>
<file context>
@@ -170,6 +207,8 @@ export default function TaskFormDialog(props: Props) {
measureId: task?.measure?.id ?? measureId ?? "",
deadline: task?.deadline?.split("T")[0] ?? "",
+ recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "",
+ recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1,
},
});
</file context>
| "delete": "Supprimer" | ||
| }, | ||
| "recurringBadge": { | ||
| "tooltip_one": "Se répète tous les {{count}} {{unit}}", |
There was a problem hiding this comment.
P2: The recurringBadge tooltip renders improperly in French for singular counts. tooltip_one should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1429:
<comment>The recurringBadge tooltip renders improperly in French for singular counts. `tooltip_one` should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").</comment>
<file context>
@@ -1413,6 +1424,10 @@
"delete": "Supprimer"
+ },
+ "recurringBadge": {
+ "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+ "tooltip_other": "Se répète tous les {{count}} {{unit}}"
}
</file context>
| }, | ||
| "recurringBadge": { | ||
| "tooltip_one": "Se répète tous les {{count}} {{unit}}", | ||
| "tooltip_other": "Se répète tous les {{count}} {{unit}}" |
There was a problem hiding this comment.
P3: The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (2 Semaine, 2 Année). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1430:
<comment>The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (`2 Semaine`, `2 Année`). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.</comment>
<file context>
@@ -1413,6 +1424,10 @@
+ },
+ "recurringBadge": {
+ "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+ "tooltip_other": "Se répète tous les {{count}} {{unit}}"
}
},
</file context>
Signed-off-by: Steven Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
a6a0ac5 to
d1a15c8
Compare
Summary by cubic
Add recurring tasks with interval unit and count across API, UI, CLI, and integrations. A background worker advances overdue recurring tasks; validations and errors are stricter and localized, and a migration stores recurrence data with DB constraints.
Tests
+336-0Coredata
+233-39task_recurrence_interval_unitenum and nullablerecurrence_interval_unit/recurrence_interval_countcolumns with a check constraint; extended Task model/queries and loader to claim the next overdue recurring task with FOR UPDATE SKIP LOCKED.GraphQL API
+61-27Task.recurrenceIntervalUnitandTask.recurrenceIntervalCountplus enum and input fields; updated resolvers and types to pass recurrence fields through.MCP
+82-31prb (CLI)
+62-16--recurrence-unitand--recurrence-counttotask createandtask update; support clearing on update (count 0 clears) with flag validation and helpful errors.Service
+295-31probodto run every 15 minutes; enforced create/update validation (pair unit/count, require deadline when recurring) with clearer errors.Package:
n8n-node+111-0App: console
+137-9en-US/fr-FRstrings for units, errors, and tooltips.Written for commit d1a15c8. Summary will update on new commits.